package handler;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

import static io.netty.handler.codec.http.HttpHeaderNames.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;

public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

  private String url;

  public HttpFileServerHandler(String url){
    this.url = url;
  }

  protected void channelRead0(ChannelHandlerContext ctx,
      FullHttpRequest request) throws Exception {
    if (!request.getDecoderResult().isSuccess()){
      sendError(ctx,BAD_REQUEST);
      return;
    }
    if(request.getMethod() != HttpMethod.GET){
      sendError(ctx,METHOD_NOT_ALLOWED);
      return;
    }
    final String uri = request.getUri();
    final String path = sanitizeUri(uri);
    if (path == null){
      sendError(ctx,FORBIDDEN);
      return;
    }
    File file = new File(path);
    //isHidden:测试此抽象路径名指定的文件是否是一个隐藏文件。
    //exists 文件是否存在
    if (file.isHidden() || !file.exists()){
      sendError(ctx,NOT_FOUND);
      return;
    }
    //抽象路径名表示的文件是否是一个目录。
    if (file.isDirectory()){
      if (uri.endsWith("/")){
        sendListing(ctx,file);
      }else{
        sendRedirect(ctx,uri + '/');
      }
      return;
    }
    //判断是一个标准文件时,表示为下载文件,用只读模式打开文件
    if (!file.isFile()){
        sendError(ctx,FORBIDDEN);
      return;
    }
    RandomAccessFile randomAccessFile = null;
    try {
      //只以读的方式打开文件
      randomAccessFile = new RandomAccessFile(file,"r");
    }catch (Exception e){
      e.printStackTrace();
      sendError(ctx,NOT_FOUND);
      return;
    }
    long fileLength = randomAccessFile.length();
    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,OK);
    HttpUtil.setContentLength(response,fileLength);
    setContentTypeHeader(response,file);
    //判断当前链接是否活动
    if (HttpUtil.isKeepAlive(request)){
      //KEEP_ALIVE 表示支持长链接
      response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
      response.headers().set(CONTENT_DISPOSITION, "attachment;filename="+
              new String(file.getName().getBytes(),"iso-8859-1"));
    }
    ctx.write(response);
    //通过ChunkedFile 直接将文件夹写入发送缓冲区
    ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile,0,fileLength,8192),
        ctx.newProgressivePromise());
    sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
      public void operationProgressed(ChannelProgressiveFuture future, long progress,
          long total) throws Exception {
        if (total < 0){
          System.out.println("Transfer progress: " + progress);
        }else{
          System.out.println("Transfer progress: " + progress + " / " + total);
        }
      }

      public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture)
          throws Exception {
        System.out.println("Transfer complete.");
      }
    });
    //如果使用chunked编码,最后需要发送一个编码结束的空消息体,
    //将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区,标识所有的消息已经发送完成
    ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
    if (!HttpUtil.isKeepAlive(request)) {
      //关闭Channel其与指定的相关联的ChannelFuture
      lastContentFuture.addListener(ChannelFutureListener.CLOSE);
    }
  }

  private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
  /**
   * 解析url地址
   * @param uri
   * @return
   */
  private String sanitizeUri(String uri){
    try {
      uri = URLDecoder.decode(uri,"utf-8");
    }catch (UnsupportedEncodingException e){
      try {
        uri = URLDecoder.decode(uri,"iso-8859-1");
      }catch (UnsupportedEncodingException e1){
        e1.printStackTrace();
      }
    }
    if (!uri.startsWith(url)){
      return null;
    }
    if (!uri.startsWith("/")){
      return null;
    }
    //将文件路径分割符替换为本地系统的文件分隔符
    uri = uri.replace('/',File.separatorChar);
    // contains 当且仅当此字符串包含指定的 char 值序列时，返回 true。
    //separator 与系统有关的默认名称分隔符，为了方便，它被表示为一个字符串
    if (uri.contains(File.separator + '.')
        || uri.contains('.' + File.separator)
        ||  uri.startsWith(".") || uri.endsWith(".")
        || INSECURE_URI.matcher(uri).matches()){
      return null;
    }
    return System.getProperty("user.dir") + File.separator + uri;
  }
  private static final Pattern ALLOWED_FILE_NAME = Pattern
          .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

  /**
   * 打开文件目录
   * @param ctx
   * @param dir
   */
  private static void sendListing(ChannelHandlerContext ctx, File dir) {
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK);
    response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
    StringBuilder buf = new StringBuilder();
    String dirPath = dir.getPath();
    buf.append("<!DOCTYPE html>\r\n");
    buf.append("<html><head><title>");
    buf.append(dirPath);
    buf.append(" 目录：");
    buf.append("</title></head><body>\r\n");
    buf.append("<h3>");
    buf.append(dirPath).append(" 目录：");
    buf.append("</h3>\r\n");
    buf.append("<ul>");
    buf.append("<li>链接：<a href=\"../\">..</a></li>\r\n");

    for (File f : dir.listFiles()) {
      //canRead 应用程序是否可以读取此抽象路径名表示的文件
      if (f.isHidden() || !f.canRead()) {
        continue;
      }
      String name = f.getName();
      if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
        continue;
      }
      buf.append("<li>链接：<a href=\"");
      buf.append(name).append("/");
      buf.append("\">");
      buf.append(name);
      buf.append("</a></li>\r\n");
    }
    buf.append("</ul></body></html>\r\n");
    ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
    response.content().writeBytes(buffer);
    buffer.release();
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  /**
   * 重定向
   * @param ctx
   * @param newUri
   */
  private static void sendRedirect(ChannelHandlerContext ctx,String newUri){
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,OK);
    response.headers().set(LOCATION,newUri);
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void sendError(ChannelHandlerContext ctx,HttpResponseStatus status){
    FullHttpResponse response = new
            DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,
            Unpooled.copiedBuffer("Failure " + status.toString()
                    + "\r\n", CharsetUtil.UTF_8));
    response.headers().set(CONTENT_TYPE,"text/plain; charset=UTF-8");
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  /**
   * 设置响应头
   * @param response
   * @param file
   */
  private static void setContentTypeHeader(HttpResponse response,File file){
    MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
    response.headers().set(CONTENT_TYPE,mimeTypesMap.getContentType(file.getPath()));
  }
}
